package br.com.foxinline.generico;

import br.com.foxinline.modelo.Anexo;
import br.com.foxinline.modelo.Bairro;
import br.com.foxinline.modelo.Cidade;
import br.com.foxinline.modelo.Endereco;
import br.com.foxinline.modelo.Imovel;
import br.com.foxinline.modelo.Usuario;
import br.com.foxinline.servico.AnexoServico;
import br.com.foxinline.servico.BairroServico;
import br.com.foxinline.servico.CidadeServico;
import br.com.foxinline.servico.EnderecoServico;
import br.com.foxinline.servico.ProprietarioServico;
import br.com.foxinline.servico.UsuarioServico;
import br.com.foxinline.vo.CamposVO;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.EJB;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

/**
 *
 * @author laverson
 */
public class ServicoHistoricoGenerico<T> {

    @EJB
    CidadeServico cidadeServico;
    @EJB
    UsuarioServico usuarioServico;
    @EJB
    BairroServico bairroServico;
    @EJB
    EnderecoServico enderecoServico;
    @EJB
    ProprietarioServico proprietarioServico;
    @EJB
    AnexoServico anexoServico;
    @PersistenceContext
    private EntityManager entityManager;
    private Class<T> entity;
    boolean add = false;
    boolean recursivo;
    private List<CamposVO> campos;

    public ServicoHistoricoGenerico(Class<T> entity) {
        this.entity = entity;
        this.campos = new ArrayList<CamposVO>();
        this.recursivo = false;
    }

    /**
     * Compara se os campos do parâmetro histórico são diferentes do parâmetro
     * do atual. Adiciona os campos que são diferentes no field campos para
     * visualização
     *
     * <br/> <p> <b>Author:</b> Diego Laverson</p>
     *
     * @param historico
     * @param atual
     * @param restaurar se true retorna o <b>atual</b\> com as modificações
     * feitas no <b>historico</b>
     *
     * @return objeto contendo os campos que são diferentes
     */
    public Object comparar(Object historico, Object atual, boolean restaurar) {

        Object entidade = null;
        add = false;

        if (restaurar) {
            entidade = atual;
        } else {
            try {
                entidade = historico != null ? historico.getClass().newInstance() : atual.getClass().newInstance();

            } catch (InstantiationException ex) {
                Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IllegalAccessException ex) {
                Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        campos = new ArrayList<CamposVO>();

        try {

            Field[] fields = entidade != null ? entidade.getClass().getDeclaredFields() : entity.getDeclaredFields();

            for (Field f : fields) {

                f.setAccessible(true);

                if (!campoIgnorado(f.getName())) {

                    verificarCoordenada(historico, atual);
                    verificarEndereco(f, historico, atual);
                    verificarCidade(f, historico, atual);
                    verificarBairro(f, historico, atual);
                    verificarListas(f, historico, atual);

                    // Compara se o valor do 'f' é !null e !"" para cada um dos objetos, historico e atual,
                    // pois se o historico for null então não é possível executar o .equals();
                    // Nesse caso a comparação passa a ser verificada pelo field do objeto atual;
                    boolean dif = false;

                    if (f != null && historico != null && f.get(historico) != null && !f.get(historico).equals("")) {

                        if (atual != null && f.get(atual) != null) {
                            dif = !f.get(historico).equals(f.get(atual));
                        } else {
                            dif = f.get(historico) != null;
                        }

                    } else if (f != null && atual != null && f.get(atual) != null && !f.get(atual).equals("")) {

                        if (historico != null && f.get(historico) != null) {
                            dif = !f.get(atual).equals(f.get(historico));
                        } else {
                            dif = f.get(atual) != null;
                        }
                    }

                    // Se os campos dos objetos forem diferentes
                    if (dif) {

                        //  Obtem o nome do field e deixa a primeira letra maiuscula
                        String nome = f.getName();
                        StringBuilder sb = new StringBuilder(nome);
                        nome = sb.replace(0, 1, nome.substring(0, 1).toUpperCase()).toString();
                        nome = nomeEntidadeCampo(nome, historico, atual);

                        Field field = entidade.getClass().getDeclaredField(f.getName());
                        field.setAccessible(true);

                        if ((historico != null && !(historico instanceof Endereco) && f.get(historico) != null && f.get(historico) instanceof Endereco)
                                || (atual != null && !(atual instanceof Endereco) && f.get(atual) != null && f.get(atual) instanceof Endereco)) {

                            comparar((Endereco) f.get(historico), (Endereco) f.get(atual));
                            field.set(entidade, f.get(historico));

                            continue;
                        }
                        if ((historico != null && f.get(historico) != null && f.get(historico) instanceof Imovel)
                                || ((atual != null && f.get(atual) != null && f.get(atual) instanceof Imovel))) {
                            continue;
                        }

                        // Se o field 'f' nao for uma entidade entao ele eh simplesmente adicionado
                        // em 'campos', pois nao ha nessecidade de uma validacao mais especifica
                        if (historico != null && atual != null) {

                            campos.add(new CamposVO(nome, f.get(historico), f.get(atual)));
                            field.set(entidade, f.get(historico));

                        } else if (historico == null && atual == null) {

                            campos.add(new CamposVO(nome, null, null));
                            field.set(entidade, historico);

                        } else if (historico == null && atual != null) {

                            campos.add(new CamposVO(nome, null, f.get(atual)));
                            field.set(entidade, historico);

                        } else if (historico != null && atual == null) {

                            campos.add(new CamposVO(nome, f.get(historico), null));
                            field.set(entidade, f.get(historico));
                        } else {

                            campos.add(new CamposVO(nome, f.get(historico), f.get(atual)));
                            field.set(entidade, f.get(historico));
                        }

                        add = true;
                    }
                }
            }
        } catch (Throwable t) {
            System.err.println(t);
        }

        recursivo = false;
        return entidade;
    }

    public void comparar(Object historico, Object atual) {

        Object entidade = null;

        try {
            entidade = historico != null ? historico.getClass().newInstance() : atual.getClass().newInstance();

        } catch (InstantiationException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        }

        try {

            Field[] fields = Endereco.class.getDeclaredFields();

            for (Field f : fields) {

                f.setAccessible(true);

                if (!campoIgnorado(f.getName())) {

                    // Compara se o valor do 'f' é !null e !"" para cada um dos objetos, historico e atual,
                    // pois se o historico for null então não é possível executar o .equals();
                    // Nesse caso a comparação passa a ser verificada pelo field do objeto atual;
                    boolean dif = false;

                    if (f != null && historico != null && f.get(historico) != null && !f.get(historico).equals("")) {

                        if (atual != null && f.get(atual) != null) {
                            dif = !f.get(historico).equals(f.get(atual));
                        } else {
                            dif = f.get(historico) != null;
                        }

                    } else if (f != null && atual != null && f.get(atual) != null && !f.get(atual).equals("")) {

                        if (historico != null && f.get(historico) != null) {
                            dif = !f.get(atual).equals(f.get(historico));
                        } else {
                            dif = f.get(atual) != null;
                        }
                    }

                    // Se os campos dos objetos forem diferentes
                    if (dif) {

                        //  Obtem o nome do field e deixa a primeira letra maiuscula
                        String nome = f.getName();
                        StringBuilder sb = new StringBuilder(nome);
                        nome = sb.replace(0, 1, nome.substring(0, 1).toUpperCase()).toString();
                        nome = nomeEntidadeCampo(nome, historico, atual);

                        Field field = entidade.getClass().getDeclaredField(f.getName());
                        field.setAccessible(true);

                        // Se o field 'f' nao for uma entidade entao ele eh simplesmente adicionado
                        // em 'campos', pois nao ha nessecidade de uma validacao mais especifica

                        campos.add(new CamposVO(nome, f.get(historico), f.get(atual)));


                        add = true;
                    }
                }
            }
        } catch (Throwable t) {
            System.err.println(t);
        }
    }

    /**
     * Pesquisar todos os registros da tabela historico de 'entity'
     *
     * <br/> <p> <b>Author:</b> Diego Laverson</p>
     *
     * @param id da entidade a ser pesquisada
     * @return todos os registros que tiverem id igual ao passado por parâmetro
     * e a 'data_final' nao for nula
     */
    public List<Object[]> findQueryHistory(Long id) {
        String sql = "SELECT * FROM " + entity.getSimpleName() + "_hist "
                + "where data_final is not null and id = " + id + " "
                + "order by codigo asc";

        Query historyQuery = entityManager.createNativeQuery(sql);

        return historyQuery.getResultList();
    }

    /**
     * Pesquisa e retorna o Endereco na tabela historico a versao do endereco
     * anterior ao endereco 'atual'
     *
     * <br/> <p> <b>Author:</b> Diego Laverson</p>
     *
     * @param historico
     * @param atual
     * @return endereco cuja versao seja menor que aversao 'atual'
     */
    private Endereco getEnderecoHistorico(Long id, Integer versao) {

        String sqlEnderecoHist = "select * from endereco_hist where id = " + id;

        if (versao != null) {
            sqlEnderecoHist += " and versao = " + (versao - 1);
        }

        List<Object[]> objects = entityManager.createNativeQuery(sqlEnderecoHist).getResultList();

        return instanciarEnderecoPorListArrayObject(objects);
    }

    /**
     * Pesquisar o Endereco na tabela historico que tiver id igual ao
     * endereco_id e a data_final for maior que a data_final_proprietario.
     *
     * <br/> <p> <b>Author:</b> Diego Laverson</P>
     *
     * @param enderecoId
     * @param dataFinalProprietario
     * @return
     */
    public Endereco getEnderecoHistoricoPorDataProprietario(Long enderecoId, Date dataFinalProprietario, Date dataInicialProprietario) {

        String sqlEnderecoHist = "select * from endereco_hist "
                + "where id = " + enderecoId + " "
                + "and ((data_final is not null and data_final > '" + dataFinalProprietario + "') "
                + "or (data_inicial < '" + dataInicialProprietario + "'))"
                + " limit 1";

        List<Object[]> objects = entityManager.createNativeQuery(sqlEnderecoHist).getResultList();

        return instanciarEnderecoPorListArrayObject(objects);
    }

    /**
     * * Pesquisar e retorna o Bairro na tabela historico a versao do bairro
     * anterior ao bairro 'atual'
     *
     * <br/> <p> <b>Author:</b> Diego Laverson</P>
     *
     * @param historico
     * @param atual
     * @return bairro com versao menor que a do historico
     */
    private Bairro getBairroHistorico(Bairro historico, Bairro atual) {

        String sqlEnderecoHist = "select * from bairro_hist where id = " + historico.getId() + " and versao = " + (atual.getVersao() - 1);

        List<Object[]> objects = entityManager.createNativeQuery(sqlEnderecoHist).getResultList();

        Bairro bairroHistorico = new Bairro();

        for (Object[] object : objects) {

            bairroHistorico.setNome((String) object[2]);
            bairroHistorico.setUf((String) object[3]);

            bairroHistorico.setCidade(new Cidade());
            bairroHistorico.getCidade().setId((Long) object[4]);

            bairroHistorico.setUsuarioAtualizacao(new Usuario());
            bairroHistorico.getUsuarioAtualizacao().setId((Long) object[8]);
        }

        return bairroHistorico;
    }

    public List<CamposVO> getCampos() {
        return campos;
    }

    public void setCampos(List<CamposVO> campos) {
        this.campos = campos;
    }

    public boolean isAdd() {
        return add;
    }

    public void setAdd(boolean add) {
        this.add = add;
    }

    public EntityManager getEntityManager() {
        return entityManager;
    }

    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    /**
     * Verifica se os objetos historico e atual sao instancias de endereco e se
     * nao sao nulos
     *
     * @param f
     * @param historico
     * @param entidade
     * @param atual
     * @throws NoSuchFieldException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws SecurityException
     */
    private void verificarEndereco(Field f, Object historico, Object atual) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, SecurityException {

        try {

            if (historico instanceof Endereco || atual instanceof Endereco) {
                return;
            }

            if (f.get(historico) instanceof Endereco || f.get(atual) instanceof Endereco) {

                Field field = historico != null ? historico.getClass().getDeclaredField(f.getName()) : atual.getClass().getDeclaredField(f.getName());
                field.setAccessible(true);

                Long idHistorico = historico != null && f.get(historico) != null ? ((Endereco) f.get(historico)).getId() : null;
                Long idAtual = atual != null && f.get(atual) != null ? ((Endereco) f.get(atual)).getId() : null;

                Endereco enderecoHistorico = new Endereco();

                if (idHistorico != null) {

                    if (atual != null && f.get(atual) != null) {
                        enderecoHistorico = getEnderecoHistorico(idHistorico, ((Endereco) f.get(atual)).getVersao());
                        field.set(historico, enderecoHistorico);
                    }
                }

                Endereco enderecoAtual;

                if (idAtual != null) {

                    enderecoAtual = getEnderecoHistorico(idAtual, ((Endereco) f.get(atual)).getVersao() + 1);

                    if (!enderecoHistorico.equals(enderecoAtual)) {
                        field.set(atual, enderecoAtual);
                    }
                }
            }

        } catch (NoSuchFieldException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void verificarCoordenada(Object historico, Object atual) {
        if ((historico != null && historico instanceof Bairro)
                || (atual != null && atual instanceof Bairro)) {

            // TODO: verificar necessidade do histórico das coordenadas
            ((Bairro) historico).setCoordenadas(((Bairro) atual).getCoordenadas());
//            if (((Bairro) historico).getCoordenadas() != null
//                    && !((Bairro) historico).getCoordenadas().isEmpty()) {
//                
//            }
        }
    }

    private void verificarCidade(Field f, Object historico, Object atual) {
        try {

            if ((historico != null && historico instanceof Cidade)
                    || (atual != null && atual instanceof Cidade)) {
                return;
            }

            if ((historico != null && historico instanceof Endereco)
                    || (atual != null && atual instanceof Endereco)) {
                if (historico == null) {
                    historico = new Endereco();
                }
                if (atual == null) {
                    atual = new Endereco();
                }
            }

            if (f.get(historico) instanceof Cidade || f.get(atual) instanceof Cidade) {

                Field field = historico != null ? historico.getClass().getDeclaredField(f.getName()) : atual.getClass().getDeclaredField(f.getName());
                field.setAccessible(true);

                Long idCidadeHistorico = ((Cidade) f.get(historico)).getId();
                Long idCidadeAtual = ((Cidade) f.get(atual)).getId();

                if (idCidadeHistorico != null) {
                    f.set(historico, cidadeServico.find(idCidadeHistorico));
                }
                if (idCidadeAtual != null) {
                    f.set(atual, cidadeServico.find(idCidadeAtual));


                }
            }
        } catch (NoSuchFieldException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void verificarBairro(Field f, Object historico, Object atual) {

        try {

//            if (historico instanceof Bairro || atual instanceof Bairro) {
//                return;
//            }

            if ((historico != null && historico instanceof Bairro)
                    || (atual != null && atual instanceof Bairro)) {
                if (historico == null) {
                    historico = new Bairro();
                }
                if (atual == null) {
                    atual = new Bairro();
                }
            }

            if ((historico != null && f.get(historico) instanceof Bairro)
                    || (atual != null && f.get(atual) instanceof Bairro)) {

                Field field = historico != null ? historico.getClass().getDeclaredField(f.getName()) : atual.getClass().getDeclaredField(f.getName());
                field.setAccessible(true);

                Long idHistorico = ((Bairro) f.get(historico)).getId();
                Long idAtual = ((Bairro) f.get(atual)).getId();

                if (idHistorico != null) {

                    f.set(historico, bairroServico.find(idHistorico));
                }
                if (idAtual != null) {

                    f.set(atual, bairroServico.find(idAtual));
                }

                if (!f.get(atual).equals(f.get(historico))) {
                    campos.add(new CamposVO(Bairro.class.getSimpleName(), ((Bairro) f.get(historico)).getNome(), ((Bairro) f.get(atual)).getNome()));
                }

            }

        } catch (NoSuchFieldException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class
                    .getName()).log(Level.SEVERE, null, ex);
        }
    }

    private Endereco instanciarEnderecoPorListArrayObject(List<Object[]> objects) {

        Endereco endereco = new Endereco();

        for (Object[] object : objects) {

            endereco = instanciarEnderecoPorArrayObject(object);

        }

        return endereco;
    }

    protected Endereco instanciarEnderecoPorArrayObject(Object[] object) {
        Endereco endereco = new Endereco();

        endereco.setId((Long) object[0]);
        endereco.setBairro((String) object[1]);
        endereco.setCep((String) object[2]);
        endereco.setCompl((String) object[5]);
        endereco.setLogradouro((String) object[7]);
        endereco.setMunicipio((String) object[8]);
        endereco.setNumero((String) object[9]);
        endereco.setUf((String) object[10]);
        endereco.setNomeclog((String) object[11]);
        endereco.setNomeslog((String) object[12]);

        if (((Long) object[13]) != null) {
            endereco.setBairroDne(new Bairro());
            endereco.getBairroDne().setId((Long) object[13]);
        }

        if (((Long) object[14]) != null) {
            endereco.setImovel(new Imovel());
            endereco.getImovel().setId((Long) object[14]);
        }

        endereco.setDataAtualizacao((Date) object[16]);
        endereco.setVersao((Integer) object[17]);

        if (((Long) object[18]) != null) {
            endereco.setUsuarioAtualizacao(new Usuario());
            endereco.getUsuarioAtualizacao().setId((Long) object[18]);
        }

        endereco.setAtivo((Boolean) object[19]);

        return endereco;
    }

    private String nomeEntidadeCampo(String nome, Object historico, Object atual) {

        if (historico != null) {
            if (historico instanceof Endereco) {
                return Endereco.class.getSimpleName() + " - " + nome;
            }
            if (historico instanceof Anexo) {
                return Anexo.class.getSimpleName();
            }
        }

        if (atual != null) {
            if (atual instanceof Endereco) {
                return Endereco.class.getSimpleName() + " - " + nome;
            }
            if (atual instanceof Anexo) {
                return Anexo.class.getSimpleName();
            }
        }

        return nome;
    }

    private void verificarListas(Field f, Object historico, Object atual) {
        verificarListaAnexo(f, historico, atual);
        //verificarListaProprietario(f, historico, atual);
        //verificarListaMorador(f, historico, atual);
    }

    private void verificarListaAnexo(Field f, Object historico, Object atual) {

        try {

            if (historico != null && f.get(historico) != null && f.get(historico) instanceof List) {

                if (((List) f.get(historico)).isEmpty()) {
                    f.set(historico, null);
                }
            }

            if (atual != null && f.get(atual) != null && f.get(atual) instanceof List) {

                if (((List) f.get(atual)).isEmpty()) {
                    f.set(atual, null);
                }
            }

        } catch (IllegalArgumentException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(ServicoHistoricoGenerico.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private boolean campoIgnorado(String name) {
        List<String> camposIgnorados = Arrays.asList(
                "id",
                "serialversionuid",
                "nomeclog",
                "nomeslog",
                "bairrodne",
                "dataatualizacao",
                "versao",
                "uf",
                "createat");

        return camposIgnorados.contains(name.toLowerCase());
    }
}
